1 /** 2 * Unrolled Linked List. 3 * Copyright: © 2015 Economic Modeling Specialists, Intl. 4 * Authors: Brian Schott 5 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 */ 7 8 module containers.unrolledlist; 9 10 private import core.lifetime : move; 11 private import containers.internal.node : shouldAddGCRange; 12 private import std.experimental.allocator.mallocator : Mallocator; 13 14 version (X86_64) 15 version (LDC) 16 version = LDC_64; 17 18 /** 19 * Unrolled Linked List. 20 * 21 * Nodes are (by default) sized to fit within a 64-byte cache line. The number 22 * of items stored per node can be read from the $(B nodeCapacity) field. 23 * See_also: $(LINK http://en.wikipedia.org/wiki/Unrolled_linked_list) 24 * Params: 25 * T = the element type 26 * Allocator = the allocator to use. Defaults to `Mallocator`. 27 * supportGC = true to ensure that the GC scans the nodes of the unrolled 28 * list, false if you are sure that no references to GC-managed memory 29 * will be stored in this container. 30 * cacheLineSize = Nodes will be sized to fit within this number of bytes. 31 */ 32 struct UnrolledList(T, Allocator = Mallocator, 33 bool supportGC = shouldAddGCRange!T, size_t cacheLineSize = 64) 34 { 35 this(this) @disable; 36 37 private import std.experimental.allocator.common : stateSize; 38 39 static if (stateSize!Allocator != 0) 40 { 41 /// No default construction if an allocator must be provided. 42 this() @disable; 43 44 /** 45 * Use the given `allocator` for allocations. 46 */ 47 this(Allocator allocator) 48 in 49 { 50 assert(allocator !is null, "Allocator must not be null"); 51 } 52 do 53 { 54 this.allocator = allocator; 55 } 56 } 57 58 ~this() nothrow 59 { 60 scope (failure) assert(false, "UnrolledList destructor threw an exception"); 61 clear(); 62 } 63 64 /** 65 * Removes all items from the list 66 */ 67 void clear() 68 { 69 Node* previous; 70 Node* current = _front; 71 while (current !is null) 72 { 73 previous = current; 74 current = current.next; 75 76 static if (useGC) 77 { 78 import core.memory: GC; 79 GC.removeRange(previous); 80 } 81 allocator.dispose(previous); 82 } 83 _length = 0; 84 _front = null; 85 _back = null; 86 } 87 88 /** 89 * Inserts the given item into the end of the list. 90 * 91 * Returns: a pointer to the inserted item. 92 */ 93 T* insertBack(T item) 94 { 95 ContainerStorageType!T* result; 96 if (_back is null) 97 { 98 assert (_front is null, "front/back nullness mismatch"); 99 _back = allocateNode(move(mutable(item))); 100 _front = _back; 101 result = &_back.items[0]; 102 } 103 else 104 { 105 size_t index = _back.nextAvailableIndex(); 106 if (index >= nodeCapacity) 107 { 108 Node* n = allocateNode(move(mutable(item))); 109 n.prev = _back; 110 _back.next = n; 111 _back = n; 112 index = 0; 113 result = &n.items[0]; 114 } 115 else 116 { 117 _back.items[index] = move(mutable(item)); 118 _back.markUsed(index); 119 result = &_back.items[index]; 120 } 121 } 122 _length++; 123 assert (_back.registry <= fullBitPattern, "Overflow"); 124 return cast(T*) result; 125 } 126 127 /** 128 * Inserts the given range into the end of the list 129 */ 130 void insertBack(R)(auto ref R range) 131 { 132 foreach (ref r; range) 133 insertBack(r); 134 } 135 136 /// ditto 137 T* opOpAssign(string op)(T item) if (op == "~") 138 { 139 return insertBack(item); 140 } 141 142 /// ditto 143 alias put = insertBack; 144 145 /// ditto 146 alias insert = insertBack; 147 148 /** 149 * Inserts the given item in the frontmost available cell, which may put the 150 * item anywhere in the list as removal may leave gaps in list nodes. Use 151 * this only if the order of elements is not important. 152 * 153 * Returns: a pointer to the inserted item. 154 */ 155 T* insertAnywhere(T item) @trusted 156 { 157 Node* n = _front; 158 while (n !is null) 159 { 160 size_t i = n.nextAvailableIndex(); 161 if (i >= nodeCapacity) 162 { 163 if (n.next is null) 164 { 165 assert (n is _back, "Wrong _back"); 166 break; 167 } 168 n = n.next; 169 continue; 170 } 171 n.items[i] = move(mutable(item)); 172 n.markUsed(i); 173 _length++; 174 assert (n.registry <= fullBitPattern, "Overflow"); 175 return cast(T*) &n.items[i]; 176 } 177 n = allocateNode(move(mutable(item))); 178 _length++; 179 auto retVal = cast(T*) &n.items[0]; 180 if (_front is null) 181 { 182 assert(_back is null, "front/back nullness mismatch"); 183 _front = n; 184 } 185 else 186 { 187 n.prev = _back; 188 _back.next = n; 189 } 190 _back = n; 191 assert (_back.registry <= fullBitPattern, "Overflow"); 192 return retVal; 193 } 194 195 /// Returns: the length of the list 196 size_t length() const nothrow pure @property @safe @nogc 197 { 198 return _length; 199 } 200 201 /// Returns: true if the list is empty 202 bool empty() const nothrow pure @property @safe @nogc 203 { 204 return _length == 0; 205 } 206 207 /** 208 * Removes the first instance of the given item from the list. 209 * 210 * Returns: true if something was removed. 211 */ 212 bool remove(ref const T item) 213 { 214 if (_front is null) 215 return false; 216 bool retVal; 217 loop: for (Node* n = _front; n !is null; n = n.next) 218 { 219 foreach (i; 0 .. nodeCapacity) 220 { 221 if (!n.isFree(i) && n.items[i] == item) 222 { 223 n.markUnused(i); 224 --_length; 225 retVal = true; 226 if (n.registry == 0) 227 deallocateNode(n); 228 else if (shouldMerge(n, n.next)) 229 mergeNodes(n, n.next); 230 else if (shouldMerge(n.prev, n)) 231 mergeNodes(n.prev, n); 232 break loop; 233 } 234 } 235 } 236 return retVal; 237 } 238 bool remove(const T item) { return remove(item); } 239 240 /// Pops the front item off of the list 241 void popFront() 242 { 243 moveFront(); 244 assert (_front is null || _front.registry != 0, "Node is non-null but empty"); 245 } 246 247 /// Pops the front item off of the list and returns it 248 T moveFront() 249 in 250 { 251 assert (!empty(), "Accessing .moveFront of empty UnrolledList"); 252 assert (_front.registry != 0, "Empty node"); 253 } 254 do 255 { 256 version (LDC_64) 257 { 258 import ldc.intrinsics : llvm_cttz; 259 size_t index = llvm_cttz(_front.registry, true); 260 } 261 else 262 { 263 import containers.internal.backwards : bsf; 264 size_t index = bsf(_front.registry); 265 } 266 T r = move(_front.items[index]); 267 _front.markUnused(index); 268 _length--; 269 if (_front.registry == 0) 270 { 271 auto f = _front; 272 if (_front.next !is null) 273 _front.next.prev = null; 274 assert (_front.next !is _front, "Infinite loop"); 275 _front = _front.next; 276 if (_front is null) 277 _back = null; 278 else 279 assert (_front.registry <= fullBitPattern, "Overflow"); 280 deallocateNode(f); 281 return r; 282 } 283 if (shouldMerge(_front, _front.next)) 284 mergeNodes(_front, _front.next); 285 return r; 286 } 287 288 debug (EMSI_CONTAINERS) invariant 289 { 290 import std.string: format; 291 assert (_front is null || _front.registry != 0, format("%x, %b", _front, _front.registry)); 292 assert (_front !is null || _back is null, "_front/_back nullness mismatch"); 293 if (_front !is null) 294 { 295 const(Node)* c = _front; 296 while (c.next !is null) 297 c = c.next; 298 assert(c is _back, "_back pointer is wrong"); 299 } 300 } 301 302 /** 303 * Time complexity is O(1) 304 * Returns: the item at the front of the list 305 */ 306 ref inout(T) front() inout nothrow @property 307 in 308 { 309 assert (!empty, "Accessing .front of empty UnrolledList"); 310 assert (_front.registry != 0, "Empty node"); 311 } 312 do 313 { 314 version (LDC_64) 315 { 316 import ldc.intrinsics : llvm_cttz; 317 immutable index = llvm_cttz(_front.registry, true); 318 } 319 else 320 { 321 import containers.internal.backwards : bsf; 322 immutable index = bsf(_front.registry); 323 } 324 return *(cast(typeof(return)*) &_front.items[index]); 325 } 326 327 /** 328 * Time complexity is O(nodeCapacity), where the nodeCapacity 329 * is the number of items in a single list node. It is a constant 330 * related to the cache line size. 331 * Returns: the item at the back of the list 332 */ 333 ref inout(T) back() inout nothrow @property 334 in 335 { 336 assert (!empty, "Accessing .back of empty UnrolledList"); 337 assert (!_back.empty, "Empty node"); 338 } 339 do 340 { 341 size_t i = nodeCapacity - 1; 342 while (_back.isFree(i)) 343 i--; 344 return *(cast(typeof(return)*) &_back.items[i]); 345 } 346 347 /// Pops the back item off of the list. 348 void popBack() 349 { 350 moveBack(); 351 } 352 353 /// Removes an item from the back of the list and returns it. 354 T moveBack() 355 in 356 { 357 assert (!empty, "Accessing .moveBack of empty UnrolledList"); 358 assert (!_back.empty, "Empty node"); 359 } 360 do 361 { 362 size_t i = nodeCapacity - 1; 363 while (_back.isFree(i)) 364 { 365 if (i == 0) 366 break; 367 else 368 i--; 369 } 370 assert (!_back.isFree(i), "Empty node"); 371 T item = move(_back.items[i]); 372 _back.markUnused(i); 373 _length--; 374 if (_back.registry == 0) 375 { 376 deallocateNode(_back); 377 return item; 378 } 379 else if (shouldMerge(_back.prev, _back)) 380 mergeNodes(_back.prev, _back); 381 return item; 382 } 383 384 /// Returns: a range over the list 385 auto opSlice(this This)() const nothrow pure @nogc @trusted 386 { 387 return Range!(This)(_front); 388 } 389 390 static struct Range(ThisT) 391 { 392 @disable this(); 393 394 this(inout(Node)* current) 395 { 396 import std.format:format; 397 398 this.current = current; 399 if (current !is null) 400 { 401 version(LDC_64) 402 { 403 import ldc.intrinsics : llvm_cttz; 404 index = llvm_cttz(current.registry, true); 405 } 406 else 407 { 408 import containers.internal.backwards : bsf; 409 index = bsf(current.registry); 410 } 411 412 assert (index < nodeCapacity); 413 } 414 else 415 current = null; 416 } 417 418 ref ET front() const nothrow @property @trusted @nogc 419 { 420 return *(cast(ET*) ¤t.items[index]); 421 //return cast(T) current.items[index]; 422 } 423 424 void popFront() nothrow pure @safe @nogc 425 { 426 index++; 427 while (true) 428 { 429 430 if (index >= nodeCapacity) 431 { 432 current = current.next; 433 if (current is null) 434 return; 435 index = 0; 436 } 437 else 438 { 439 if (current.isFree(index)) 440 index++; 441 else 442 return; 443 } 444 } 445 } 446 447 bool empty() const nothrow pure @property @safe @nogc 448 { 449 return current is null; 450 } 451 452 Range save() const nothrow pure @property @safe @nogc 453 { 454 return this; 455 } 456 457 private: 458 459 alias ET = ContainerElementType!(ThisT, T); 460 const(Node)* current; 461 size_t index; 462 } 463 464 private: 465 466 import std.experimental.allocator: make, dispose; 467 import containers.internal.node : FatNodeInfo, shouldAddGCRange, 468 fullBits, shouldNullSlot; 469 import containers.internal.storage_type : ContainerStorageType; 470 import containers.internal.element_type : ContainerElementType; 471 import containers.internal.mixins : AllocatorState; 472 473 alias N = FatNodeInfo!(T.sizeof, 2, cacheLineSize); 474 enum size_t nodeCapacity = N[0]; 475 alias BookkeepingType = N[1]; 476 enum fullBitPattern = fullBits!(BookkeepingType, nodeCapacity); 477 enum bool useGC = supportGC && shouldAddGCRange!T; 478 479 static ref ContainerStorageType!T mutable(ref T value) { return *cast(ContainerStorageType!T*)&value; } 480 481 Node* _back; 482 Node* _front; 483 size_t _length; 484 mixin AllocatorState!Allocator; 485 486 Node* allocateNode(T item) 487 { 488 Node* n = make!Node(allocator); 489 static if (useGC) 490 { 491 import core.memory: GC; 492 GC.addRange(n, Node.sizeof); 493 } 494 n.items[0] = move(mutable(item)); 495 n.markUsed(0); 496 return n; 497 } 498 499 void deallocateNode(Node* n) 500 { 501 if (n.prev !is null) 502 n.prev.next = n.next; 503 if (n.next !is null) 504 n.next.prev = n.prev; 505 if (_front is n) 506 _front = n.next; 507 if (_back is n) 508 _back = n.prev; 509 510 static if (useGC) 511 { 512 import core.memory: GC; 513 GC.removeRange(n); 514 } 515 allocator.dispose(n); 516 } 517 518 static bool shouldMerge(const Node* first, const Node* second) 519 { 520 if (first is null || second is null) 521 return false; 522 version (LDC_64) 523 { 524 import ldc.intrinsics : llvm_ctpop; 525 526 immutable f = llvm_ctpop(first.registry); 527 immutable s = llvm_ctpop(second.registry); 528 } 529 else 530 { 531 import containers.internal.backwards : popcnt; 532 533 immutable f = popcnt(first.registry); 534 immutable s = popcnt(second.registry); 535 } 536 return f + s <= nodeCapacity; 537 } 538 539 void mergeNodes(Node* first, Node* second) 540 in 541 { 542 assert (first !is null, "Invalid merge"); 543 assert (second !is null, "Invalid merge"); 544 assert (second is first.next, "Invalid merge"); 545 } 546 do 547 { 548 size_t i; 549 ContainerStorageType!T[nodeCapacity] temp; 550 foreach (j; 0 .. nodeCapacity) 551 if (!first.isFree(j)) 552 temp[i++] = move(first.items[j]); 553 foreach (j; 0 .. nodeCapacity) 554 if (!second.isFree(j)) 555 temp[i++] = move(second.items[j]); 556 foreach (j; 0 .. i) 557 first.items[j] = move(temp[j]); 558 first.registry = 0; 559 foreach (k; 0 .. i) 560 first.markUsed(k); 561 assert (first.registry <= fullBitPattern, "Overflow"); 562 deallocateNode(second); 563 } 564 565 static struct Node 566 { 567 size_t nextAvailableIndex() const nothrow pure @safe @nogc 568 { 569 static if (BookkeepingType.sizeof < uint.sizeof) 570 immutable uint notReg = ~(cast(uint) registry); 571 else 572 immutable auto notReg = ~registry; 573 version (LDC_64) 574 { 575 import ldc.intrinsics : llvm_cttz; 576 return llvm_cttz(notReg, true); 577 } 578 else 579 { 580 import containers.internal.backwards : bsf; 581 return bsf(notReg); 582 } 583 } 584 585 void markUsed(size_t index) nothrow pure @safe @nogc 586 { 587 registry |= (BookkeepingType(1) << index); 588 } 589 590 void markUnused(size_t index) nothrow pure @safe @nogc 591 { 592 registry &= ~(BookkeepingType(1) << index); 593 static if (shouldNullSlot!T) 594 items[index] = null; 595 } 596 597 bool empty() const nothrow pure @safe @nogc 598 { 599 return registry == 0; 600 } 601 602 bool isFree(size_t index) const nothrow pure @safe @nogc 603 { 604 return (registry & (BookkeepingType(1) << index)) == 0; 605 } 606 607 debug(EMSI_CONTAINERS) invariant() 608 { 609 import std.string : format; 610 assert (registry <= fullBitPattern, format("%016b %016b", registry, fullBitPattern)); 611 assert (prev !is &this, "Infinite loop"); 612 assert (next !is &this, "Infinite loop"); 613 } 614 615 BookkeepingType registry; 616 ContainerStorageType!T[nodeCapacity] items; 617 Node* prev; 618 Node* next; 619 } 620 } 621 622 version(emsi_containers_unittest) unittest 623 { 624 import std.algorithm : equal; 625 import std.range : iota; 626 import std.string : format; 627 UnrolledList!ubyte l; 628 static assert (l.Node.sizeof <= 64); 629 assert (l.empty); 630 l.insert(0); 631 assert (l.length == 1); 632 assert (!l.empty); 633 foreach (i; 1 .. 100) 634 l.insert(cast(ubyte) i); 635 assert (l.length == 100); 636 assert (equal(l[], iota(100))); 637 foreach (i; 0 .. 100) 638 assert (l.remove(cast(ubyte) i), format("%d", i)); 639 assert (l.length == 0, format("%d", l.length)); 640 assert (l.empty); 641 642 assert(*l.insert(1) == 1); 643 assert(*l.insert(2) == 2); 644 assert (l.remove(1)); 645 assert (!l.remove(1)); 646 assert (!l.empty); 647 648 UnrolledList!ubyte l2; 649 l2.insert(1); 650 l2.insert(2); 651 l2.insert(3); 652 assert (l2.front == 1); 653 l2.popFront(); 654 assert (l2.front == 2); 655 assert (equal(l2[], [2, 3])); 656 l2.popFront(); 657 assert (equal(l2[], [3])); 658 l2.popFront(); 659 assert (l2.empty, format("%d", l2.front)); 660 assert (equal(l2[], cast(int[]) [])); 661 UnrolledList!int l3; 662 foreach (i; 0 .. 200) 663 l3.insert(i); 664 foreach (i; 0 .. 200) 665 { 666 auto x = l3.moveFront(); 667 assert (x == i, format("%d %d", i, x)); 668 } 669 assert (l3.empty); 670 foreach (i; 0 .. 200) 671 l3.insert(i); 672 assert (l3.length == 200); 673 foreach (i; 0 .. 200) 674 { 675 assert (l3.length == 200 - i); 676 auto x = l3.moveBack(); 677 assert (x == 200 - i - 1, format("%d %d", 200 - 1 - 1, x)); 678 } 679 assert (l3.empty); 680 } 681 682 version(emsi_containers_unittest) unittest 683 { 684 struct A { int a; int b; } 685 UnrolledList!(const(A)) objs; 686 objs.insert(A(10, 11)); 687 static assert (is (typeof(objs.front) == const)); 688 static assert (is (typeof(objs[].front) == const)); 689 } 690 691 version(emsi_containers_unittest) unittest 692 { 693 static class A 694 { 695 int a; 696 int b; 697 698 this(int a, int b) 699 { 700 this.a = a; 701 this.b = b; 702 } 703 } 704 705 UnrolledList!(A) objs; 706 objs.insert(new A(10, 11)); 707 } 708 709 // Issue #52 710 version(emsi_containers_unittest) unittest 711 { 712 UnrolledList!int list; 713 list.insert(0); 714 list.insert(0); 715 list.insert(0); 716 list.insert(0); 717 list.insert(0); 718 719 foreach (ref it; list[]) 720 it = 1; 721 722 foreach (it; list[]) 723 assert(it == 1); 724 } 725 726 // Issue #53 727 version(emsi_containers_unittest) unittest 728 { 729 UnrolledList!int ints; 730 ints.insertBack(0); 731 ints.insertBack(0); 732 733 ints.front = 1; 734 ints.back = 11; 735 736 assert(ints.front == 1); 737 assert(ints.back == 11); 738 } 739 740 // Issue #168 741 version(emsi_containers_unittest) unittest 742 { 743 import std.typecons : RefCounted; 744 alias E = RefCounted!int; 745 746 E e = E(12); 747 UnrolledList!E ints; 748 ints.insertBack(e); 749 ints.clear(); 750 // crucial: no assert failure 751 assert (e == 12); 752 } 753 754 // Issue #170 755 version(emsi_containers_unittest) unittest 756 { 757 static struct S { @disable this(this); } 758 UnrolledList!S list; 759 760 list.insert(S()); 761 list.remove(S()); 762 763 list.insert(S()); 764 S s; 765 list.remove(s); 766 }